<# .SYNOPSIS Configure Password Policy settings using enhanced SecpolFramework. .SCRIPTTYPE Computer Configuration .DESCRIPTION This script configures Windows Password Policy settings under Account Policies → Password Policy. Uses the enhanced SecpolFramework architecture with professional logging, progress tracking, and validation. PASSWORD POLICIES (8 policies): 1. Enforce password history (0-24 passwords) 2. Maximum password age (0-998 days) 3. Minimum password age (0-998 days) 4. Minimum password length (0-14 characters) 5. Minimum password length audit (1-128 characters) 6. Password must meet complexity requirements (0=Disabled, 1=Enabled) 7. Relax minimum password length legacy limits (0=Disabled, 1=Enabled) 8. Store passwords using reversible encryption (0=Disabled, 1=Enabled) .PARAMETER PolicyValues JSON array string containing 8 values corresponding to password policies (in order). Use empty strings "" to skip policies. Format: '["value1","value2","value3",...]' Array Index | Policy Name ------------|------------ 0 | Enforce password history (0-24) 1 | Maximum password age (0-998) 2 | Minimum password age (0-998) 3 | Minimum password length (0-14) 4 | Minimum password length audit (1-128) 5 | Password must meet complexity requirements (0 or 1) 6 | Relax minimum password length legacy limits (0 or 1) 7 | Store passwords using reversible encryption (0 or 1) .PARAMETER LogLevel Logging verbosity: Silent, Normal, Verbose, Debug .PARAMETER LogPath Custom log file path (optional) .PARAMETER WhatIf Shows what changes would be made without actually applying them .EXAMPLE .\Set-PasswordPolicies.ps1 '["24","90","1","14","128","1","1","0"]' Configures all 8 password policies with specified values. .EXAMPLE .\Set-PasswordPolicies.ps1 '["","90","","12","","1","",""]' Only configures: Maximum password age (90 days), Minimum password length (12 chars), Password complexity (enabled). Skips policies with empty strings. .EXAMPLE .\Set-PasswordPolicies.ps1 '["24","90","1","14","128","1","1","0"]' -WhatIf -LogLevel Verbose Configures all 8 password policies with WhatIf mode enabled and verbose logging. .NOTES - Requires administrative privileges (Run as Administrator) - Uses enhanced SecpolFramework architecture - Professional logging with script name detection - Progress tracking for user visibility #> param( [Parameter(Position=0, ValueFromRemainingArguments=$true)] [string[]]$PolicyValuesArray = @("[]"), [ValidateSet('Silent','Normal','Verbose','Debug')] [string]$LogLevel = 'Normal', [string]$LogPath = $null, [switch]$WhatIf ) # Combine all arguments into a single PolicyValues string # First, try to get the original command line with proper quotes $PolicyValues = $null try { $currentPID = $PID Write-Host "Current Process ID: $currentPID" -ForegroundColor Cyan $process = Get-CimInstance Win32_Process -Filter "ProcessId = $currentPID" if ($process) { $commandLine = $process.CommandLine Write-Host "Full command line: $commandLine" -ForegroundColor Yellow if ($commandLine) { # Get the script name for more precise regex matching $scriptName = [System.IO.Path]::GetFileName($MyInvocation.MyCommand.Path) $escapedScriptName = [regex]::Escape($scriptName) # Extract the first argument after this specific script (with all quotes intact) # Stop at known parameters: -LogLevel, -LogPath, -WhatIf, or end of string $pattern = "-File\s+`"[^`"]*\\$escapedScriptName`"\s+(.+?)(?:\s+(?:-LogLevel|-LogPath|-WhatIf)|$)" Write-Host "Using regex pattern: $pattern" -ForegroundColor DarkGray if ($commandLine -match $pattern) { $rawArgument = $matches[1].Trim() Write-Host "Raw argument extracted: $rawArgument" -ForegroundColor Magenta # Remove outer quotes if present if ($rawArgument -match '^"(.*)"$') { $PolicyValues = $matches[1] } else { $PolicyValues = $rawArgument } Write-Host "Extracted PolicyValues from command line: $PolicyValues" -ForegroundColor Green } else { Write-Host "Command line regex did not match. Command line: $commandLine" -ForegroundColor Red } } else { Write-Host "CommandLine property is null or empty" -ForegroundColor Red } } else { Write-Host "Failed to get process information for PID $currentPID" -ForegroundColor Red } } catch { Write-Host "Error extracting from command line: $($_.Exception.Message)" -ForegroundColor Red Write-Verbose "Could not extract from command line: $($_.Exception.Message)" } # Fallback: Use parameter-based approach if command line extraction failed if (-not $PolicyValues) { $PolicyValues = if ($PolicyValuesArray.Count -gt 1) { # Multiple arguments - join them back together $PolicyValuesArray -join '' } else { # Single argument - use as-is $PolicyValuesArray[0] } Write-Verbose "Using parameter-based PolicyValues: $PolicyValues" } # Password Policy Database - Enhanced with descriptions and validation ranges $PolicyDatabase = @( @{ Name = "Enforce password history" KeyGroup = "[System Access]" Key = "PasswordHistorySize" ValueType = "int" Description = "Number of unique passwords before reuse allowed (0-24)" Range = "0-24 passwords" }, @{ Name = "Maximum password age" KeyGroup = "[System Access]" Key = "MaximumPasswordAge" ValueType = "int" Description = "Maximum days before password expires (0-998)" Range = "0-998 days" }, @{ Name = "Minimum password age" KeyGroup = "[System Access]" Key = "MinimumPasswordAge" ValueType = "int" Description = "Minimum days before password can be changed (0-998)" Range = "0-998 days" }, @{ Name = "Minimum password length" KeyGroup = "[System Access]" Key = "MinimumPasswordLength" ValueType = "int" Description = "Minimum characters required for passwords (0-14)" Range = "0-14 characters" }, @{ Name = "Minimum password length audit" KeyGroup = "[Registry Values]" Key = "MACHINE\System\CurrentControlSet\Control\SAM\MinimumPasswordLengthAudit" ValueType = "dword" Description = "Audit minimum length for monitoring (1-128)" Range = "1-128 characters" }, @{ Name = "Password must meet complexity requirements" KeyGroup = "[System Access]" Key = "PasswordComplexity" ValueType = "int" Description = "Require complex passwords (0=Disabled, 1=Enabled)" Range = "0 or 1" }, @{ Name = "Relax minimum password length legacy limits" KeyGroup = "[Registry Values]" Key = "MACHINE\System\CurrentControlSet\Control\SAM\RelaxMinimumPasswordLengthLimits" ValueType = "dword" Description = "Allow longer passwords (0=Disabled, 1=Enabled)" Range = "0 or 1" }, @{ Name = "Store passwords using reversible encryption" KeyGroup = "[System Access]" Key = "ClearTextPassword" ValueType = "int" Description = "Store passwords reversibly (0=Disabled, 1=Enabled)" Range = "0 or 1" } ) # Script-wide variables $script:LogFile = $null $script:StartTime = Get-Date $script:ProcessedCount = 0 $script:SuccessCount = 0 $script:FailureCount = 0 $script:SkippedCount = 0 # Initialize logging function Initialize-LogPath { if ($LogPath) { $logDir = Split-Path $LogPath -Parent if ($logDir -and -not (Test-Path $logDir)) { New-Item -ItemType Directory -Path $logDir -Force | Out-Null } return $LogPath } # Try to get agent directory, fallback to script directory $baseDir = $PSScriptRoot try { $registryPath = if ([Environment]::Is64BitOperatingSystem) { "HKLM:\SOFTWARE\WOW6432Node\AdventNet\DesktopCentral\DCAgent" } else { "HKLM:\SOFTWARE\AdventNet\DesktopCentral\DCAgent" } $agentDir = Get-ItemProperty -Path $registryPath -Name "DCAgentInstallDir" -ErrorAction SilentlyContinue | Select-Object -ExpandProperty DCAgentInstallDir if ($agentDir -and (Test-Path $agentDir)) { $baseDir = $agentDir } } catch { Write-Verbose "Using script directory for logs" } # Create log directory and file path $auditDir = Join-Path (Join-Path $baseDir "logs") "SecurityPolicies" if (-not (Test-Path $auditDir)) { New-Item -ItemType Directory -Path $auditDir -Force | Out-Null } $timestamp = Get-Date -Format "yyyyMMdd_HHmmss" $scriptName = [System.IO.Path]::GetFileNameWithoutExtension($MyInvocation.ScriptName) if ([string]::IsNullOrEmpty($scriptName)) { $scriptName = "Set-PasswordPolicies1" } return Join-Path $auditDir "${scriptName}_$timestamp.log" } $script:LogFile = try { Initialize-LogPath } catch { $null } # Logging Functions function Write-Log { param( [Parameter(Mandatory=$true)] [string]$Message, [ValidateSet('Info','Warning','Error','Debug','Success')] [string]$Level = 'Info', [string]$Component = 'PasswordPolicy' ) $timestamp = Get-Date -Format 'yyyy-MM-dd HH:mm:ss' $logMessage = "[$timestamp] [$Level] [$Component] $Message" # Console output based on level and LogLevel setting switch ($Level) { 'Error' { if ($LogLevel -ne 'Silent') { Write-Error $Message } } 'Warning' { if ($LogLevel -notin @('Silent')) { Write-Warning $Message } } 'Success' { if ($LogLevel -notin @('Silent')) { Write-Host $Message -ForegroundColor Green } } 'Debug' { if ($LogLevel -eq 'Debug') { Write-Host $Message -ForegroundColor Gray } } 'Info' { if ($LogLevel -notin @('Silent')) { Write-Host $Message } } } # File output if LogPath is specified if ($script:LogFile) { Add-Content -Path $script:LogFile -Value $logMessage -Encoding UTF8 } } function Write-ProgressLog { param( [int]$Current, [int]$Total, [string]$Activity = "Processing Password Policies", [string]$CurrentItem = "" ) if ($LogLevel -ne 'Silent') { $percentComplete = if ($Total -gt 0) { ($Current / $Total) * 100 } else { 0 } Write-Progress -Activity $Activity -Status "Processing $Current of $Total - $CurrentItem" -PercentComplete $percentComplete } } function Test-Admin { $id = [Security.Principal.WindowsIdentity]::GetCurrent() $p = New-Object Security.Principal.WindowsPrincipal($id) return $p.IsInRole([Security.Principal.WindowsBuiltinRole]::Administrator) } function Initialize-Script { Write-Log "========== Password Policy Configuration Started ==========" Write-Log "Script: Set-PasswordPolicies1.ps1" Write-Log "User: $env:USERNAME" Write-Log "Computer: $env:COMPUTERNAME" Write-Log "PowerShell Version: $($PSVersionTable.PSVersion)" Write-Log "Log Level: $LogLevel" if ($script:LogFile) { Write-Log "Log File: $($script:LogFile)" } else { Write-Log "Logging: Console only" } if ($WhatIf) { Write-Log "WhatIf mode enabled - no changes will be applied" -Level Warning } if (-not (Test-Admin)) { Write-Log "Administrator privileges required. Please run this script as Administrator." -Level Error throw "Administrator privileges required" } Write-Log "Administrator check passed" -Level Success Write-Log "Password Policy Database contains $($PolicyDatabase.Count) policies" -Level Success return $true } # Initialize secedit file paths $exportPath = Join-Path -Path $PSScriptRoot -ChildPath "secpol_export.inf" $modifiedPath = Join-Path -Path $PSScriptRoot -ChildPath "secpol_modified.inf" if (-not (Initialize-Script)) { return } Write-Log "Exporting current security policy to: $exportPath" -Level Debug secedit /export /cfg $exportPath | Out-Null function Import-InfFile { param ([string]$filePath) $data = @{} $currentSection = "" foreach ($line in Get-Content $filePath) { $line = $line.Trim() if ($line -match "^\[(.+)\]$") { $currentSection = $matches[1] if (-not $data.ContainsKey($currentSection)) { $data[$currentSection] = @{} } } elseif ($line -and -not $line.StartsWith(";") -and $currentSection) { $splitIndex = $line.IndexOf("=") if ($splitIndex -gt 0) { $key = $line.Substring(0, $splitIndex).Trim() $value = $line.Substring($splitIndex + 1).Trim() $data[$currentSection][$key] = $value } } } return $data } $policyData = Import-InfFile -filePath $exportPath function Write-InfFile { param ( [hashtable]$policyData, [string]$filePath ) $content = @() if ($policyData.ContainsKey('Unicode')) { $content += "[Unicode]" foreach ($key in $policyData['Unicode'].Keys) { $content += "$key = $($policyData['Unicode'][$key])" } $content += "" } foreach ($section in $policyData.Keys | Where-Object { $_ -ne 'Unicode' -and $_ -ne 'Version' }) { $content += "[$section]" foreach ($key in $policyData[$section].Keys) { $content += "$key = $($policyData[$section][$key])" } $content += "" } if ($policyData.ContainsKey('Version')) { $content += "[Version]" foreach ($key in $policyData['Version'].Keys) { $content += "$key = $($policyData['Version'][$key])" } $content += "" } $content | Out-File -FilePath $filePath -Encoding ASCII } function Set-SecpolRow { param( [Parameter(Mandatory=$true)] [string]$Name, [Parameter(Mandatory=$true)] [string]$KeyGroup, [Parameter(Mandatory=$true)] [string]$Key, [Parameter(Mandatory=$true)] [string]$Value, [string]$ValueType = "" ) $section = ($KeyGroup.Trim() -replace '^\[|\]$').Trim() $keyName = $Key.Trim() $cleanValue = ($Value -replace '[\u200B-\u200D\uFEFF]', '').Trim() if ([string]::IsNullOrEmpty($cleanValue)) { Write-Log "Policy '${Name}': Value is empty or whitespace. Skipped." -Level Warning return $false } if (-not $policyData.ContainsKey($section)) { Write-Log "Creating new section: [$section]" -Level Info $policyData[$section] = @{} } # Handle Registry Values section special formatting if ($section -eq "Registry Values" -and $ValueType -eq "dword") { $cleanValue = "4,$cleanValue" Write-Log "Formatted registry value: $cleanValue" -Level Debug } Write-Log "Setting [$section] $keyName = $cleanValue for policy: $Name" -Level Info $policyData[$section][$keyName] = $cleanValue return $true } # Parse PolicyValues array string to array function Parse-PolicyValuesArray { param([string]$ArrayString) # Check if input is JSON format (starts with [ and ends with ]) if ($ArrayString -match '^\s*\[.*\]\s*$') { Write-Log "Detected JSON format input, attempting to parse..." -Level Debug try { # Parse the JSON array string $Arguments = ConvertFrom-Json $ArrayString Write-Log "Successfully parsed JSON policy values array: $($Arguments.Count) values provided" -Level Info return $Arguments } catch { Write-Log "Failed to parse as JSON: $($_.Exception.Message)" -Level Warning Write-Log "Falling back to positional argument parsing..." -Level Info } } else { Write-Log "Input is not in JSON format (doesn't start with [ ), using as positional arguments" -Level Info } # Fallback: Use PolicyValuesArray as positional arguments Write-Log "Using $($PolicyValuesArray.Count) positional arguments" -Level Info return $PolicyValuesArray } Write-Log "Arguments provided: $PolicyValues" # Parse PolicyValues array string to get individual arguments $Arguments = Parse-PolicyValuesArray -ArrayString $PolicyValues function Save-SecpolChanges { try { Write-InfFile -policyData $policyData -filePath $modifiedPath Write-Log "Updated INF file saved to $modifiedPath" -Level Success if (-not $WhatIf) { Write-Log "Importing password policy changes..." -Level Info secedit /configure /db secedit.sdb /cfg $modifiedPath /areas SECURITYPOLICY | Out-Null Write-Log "Password policy updated successfully" -Level Success } else { Write-Log "WHATIF: Would import password policy from $modifiedPath" -Level Info } } catch { Write-Log "Failed to update password policy: $($_.Exception.Message)" -Level Error throw } } # Main processing loop with comprehensive logging Write-Log "Starting password policy processing for $($PolicyDatabase.Count) policies" Write-Log "Arguments provided: $($Arguments.Count)" # Display policy information if ($LogLevel -in @('Verbose', 'Debug')) { Write-Log "Password Policy Database:" -Level Info for ($i = 0; $i -lt $PolicyDatabase.Count; $i++) { $policy = $PolicyDatabase[$i] Write-Log " [$($i+1)] $($policy.Name)" -Level Info Write-Log " Description: $($policy.Description)" -Level Debug Write-Log " Range: $($policy.Range)" -Level Debug Write-Log " KeyGroup: $($policy.KeyGroup)" -Level Debug Write-Log " Key: $($policy.Key)" -Level Debug } } for ($i = 0; $i -lt $PolicyDatabase.Count; $i++) { $script:ProcessedCount++ $policy = $PolicyDatabase[$i] Write-ProgressLog -Current ($i + 1) -Total $PolicyDatabase.Count -CurrentItem $policy.Name if ($i -ge $Arguments.Count) { Write-Log "Policy #$($i+1): '$($policy.Name)' - Not Configured (no argument provided)" -Level Info $script:SkippedCount++ continue } $arg = $arg = ([string]$Arguments[$i]).Trim() if ([string]::IsNullOrEmpty($arg)) { Write-Log "Policy #$($i+1): '$($policy.Name)' - Not Configured (empty argument)" -Level Info $script:SkippedCount++ continue } Write-Log "Processing policy #$($i+1): '$($policy.Name)' with value: '$arg'" -Level Info # Show policy details in verbose mode if ($LogLevel -in @('Verbose', 'Debug')) { Write-Log " Description: $($policy.Description)" -Level Info Write-Log " Valid Range: $($policy.Range)" -Level Info Write-Log " KeyGroup: $($policy.KeyGroup)" -Level Debug Write-Log " Key: $($policy.Key)" -Level Debug } try { if (Set-SecpolRow -Name $policy.Name -KeyGroup $policy.KeyGroup -Key $policy.Key -Value $arg -ValueType $policy.ValueType) { $script:SuccessCount++ Write-Log "Successfully processed password policy: '$($policy.Name)'" -Level Success } else { $script:FailureCount++ Write-Log "Failed to process password policy: '$($policy.Name)'" -Level Error } } catch { $script:FailureCount++ Write-Log "Exception processing password policy '$($policy.Name)': $($_.Exception.Message)" -Level Error } Write-Log "--- Policy #$($i+1) completed ---" -Level Debug } # Commit any pending password policy changes Write-Log "Committing password policy changes..." -Level Info try { Save-SecpolChanges Write-Log "Password policy changes committed successfully" -Level Success } catch { Write-Log "Failed to commit password policy changes: $($_.Exception.Message)" -Level Error $script:FailureCount++ } # Final summary and cleanup function Write-CompletionSummary { $endTime = Get-Date $duration = $endTime - $script:StartTime Write-Log "========== Password Policy Configuration Summary ==========" -Level Info Write-Log "Execution Duration: $($duration.ToString('hh\:mm\:ss'))" -Level Info Write-Log "Total Password Policies: $($PolicyDatabase.Count)" -Level Info Write-Log "Successfully Applied: $script:SuccessCount" -Level Success Write-Log "Failed: $script:FailureCount" -Level $(if ($script:FailureCount -gt 0) { 'Warning' } else { 'Info' }) Write-Log "Not Configured: $script:SkippedCount" -Level Info $actuallyProcessed = $script:SuccessCount + $script:FailureCount if ($actuallyProcessed -gt 0) { $successRate = [math]::Round(($script:SuccessCount / $actuallyProcessed) * 100, 2) Write-Log "Success Rate: $successRate% (of actually processed policies)" -Level Info } if ($script:FailureCount -gt 0) { Write-Log "Some password policies failed to apply. Check the log for details." -Level Warning Write-Log "Common issues: Invalid values, insufficient permissions, secedit errors" -Level Warning } if ($WhatIf) { Write-Log "WhatIf mode was enabled - no actual changes were made." -Level Info } Write-Log "IMPORTANT NOTES:" -Level Info Write-Log "- Password changes require system restart or 'gpupdate /force' to take full effect" -Level Info Write-Log "- Test password changes with non-administrative accounts" -Level Info Write-Log "- Verify password complexity requirements work as expected" -Level Info if ($script:LogFile) { Write-Log "Detailed log saved to: $script:LogFile" -Level Info } Write-Log "========== Password Policy Configuration Complete ==========" -Level Info } Write-CompletionSummary if ($script:FailureCount -gt 0) { exit 1 } else { exit 0 }